package no.cicero.testframework.execution;

import no.cicero.testframework.config.Config;
import no.cicero.testframework.execution.exceptions.CallException;
import no.cicero.testframework.execution.exceptions.ExecutionException;
import no.cicero.testframework.execution.exceptions.ValidationException;
import no.cicero.testframework.execution.validation.*;
import no.cicero.testframework.model.testcase.ClientObject;
import no.cicero.testframework.model.testcase.Step;
import no.cicero.testframework.model.testcase.TestCase;
import no.cicero.testframework.reporting.ReportingStep;
import no.cicero.testframework.reporting.ReportingTestCase;
import org.apache.axis.AxisFault;
import org.apache.axis.client.Call;
import org.apache.axis.client.Service;
import org.apache.axis.message.SOAPEnvelope;
import org.apache.axis.message.SOAPHeaderElement;
import org.apache.ws.security.WSConstants;
import org.apache.ws.security.message.WSSecHeader;
import org.apache.ws.security.message.WSSecUsernameToken;
import org.w3c.dom.Document;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

import javax.xml.namespace.QName;
import javax.xml.soap.SOAPElement;
import javax.xml.soap.SOAPException;
import java.net.URL;
import java.util.*;
import java.util.regex.Pattern;

public class TestCaseRunner {
    private Config configuration;
    protected TestCase testCase;
    private ReportingTestCase report = null;
    private ReportingStep stepReport = null;
    private static final String VALUE_NOT_FOUND = "Value Not Found";

    public TestCaseRunner(TestCase testCase, Config configuration, ReportingTestCase report) {
        this.testCase        = testCase;
        this.configuration   = configuration;
        this.report          = report;
    }

    public void run(){
        for(Step step: testCase.getSteps()) {
          //  if(stepReport != null)
                stepReport = report.stepStart(step);

            try {
                run(step);
 //               if(report != null)
                    report.setStepSucceeded();
			} catch(ValidationException ex){
				handleException(ex);
            } catch(CallException ex){
				ex.printStackTrace();
				handleException(ex);
            }

            stepReport = null;
        }
    }

	protected void handleException(ExecutionException ex){
		stepReport.setFailure(ex);
		report.setStepFailed();
	}

    protected void run(Step step) throws CallException, ValidationException {
		try {
			Service service = new Service();
			Call call = (Call) service.createCall();
			call.setTargetEndpointAddress(new URL(configuration.getTargetUrl(), step.getWebservice()));
			call.setOperationName(new QName(configuration.getNamespace(), step.getMethod()));

            // force Call to set SOAPAction HTTP header
            call.setProperty(Call.SOAPACTION_USE_PROPERTY, Boolean.TRUE);
            call.setProperty(Call.SOAPACTION_URI_PROPERTY, step.getMethod());

			SOAPEnvelope request = new SOAPEnvelope();
			SOAPElement operation =
					request.getBody().
					addChildElement(step.getMethod(), configuration.getNamespacePrefix(), configuration.getNamespace());
			soapSerialize(step.getArguments(), operation);

			if (configuration.isUseHeader()) {
				addSecurityHeader(request);
				addParameters(request, step);
			}
			if (configuration.isTestStructureOnly()) {
				return;
			}
			try {
				SOAPEnvelope response = call.invoke(request);
				validateSuccess(step, response);
			} catch (AxisFault ex) {
				validateFault(step, ex);
			}
        } catch (ValidationException ex) {
            throw ex;
        } catch (CallException ex) {
            throw ex;
        } catch (Exception ex) {
            throw new CallException(ex);
        }
    }
	
	private void addSecurityHeader(SOAPEnvelope request) throws Exception{
		if(configuration.getUser() == null)
			return;

		String user = configuration.getUser();
		String password = configuration.getPassword();
		
		Document asDocument = request.getAsDocument();

		WSSecUsernameToken token = new WSSecUsernameToken();
		token.setUserInfo(user, password);
		token.setPasswordType(WSConstants.PASSWORD_DIGEST);
		token.addNonce();
		token.addCreated();

		token.prepare(asDocument);

		WSSecHeader secHeader = new WSSecHeader();
		secHeader.insertSecurityHeader(asDocument);
		token.appendToHeader(secHeader);

		SOAPHeaderElement headerEl = new SOAPHeaderElement(secHeader.getSecurityHeader());
		headerEl.setActor(null);
		request.addHeader(headerEl);
	}
	
	private void addParameters(SOAPEnvelope request, Step step) throws CallException {
		Map<String, String> parameters = new HashMap<String, String>();
		parameters.putAll(configuration.getParams());
		parameters.putAll(step.getParameters());
		if (parameters == null || parameters.size() == 0)
			return;

		SOAPHeaderElement soapHeaderElement = new SOAPHeaderElement("http://www.cicero.no/XMLSchema/cfp/parameters-1.0.xsd", "parameters");
		for (Map.Entry<String, String> parameter : parameters.entrySet()) {
			if(parameter.getValue() == null)
				continue;
			try {
				SOAPElement soapElement = soapHeaderElement.addChildElement("parameter");
				soapElement.setAttribute("key", parameter.getKey());
				soapElement.addTextNode(parameter.getValue());
			} catch (SOAPException e) {
				throw new CallException("Unable add parameter " + parameter.getKey() + "=" + parameter.getValue(), e);
			}
		}
		request.addHeader(soapHeaderElement);
	}

    private void validateSuccess(Step step, SOAPEnvelope response) throws SOAPException, ValidationException{
        Node resultNode = response.getBody().getFirstChild();
        ClientObject result = soapDeserialize(resultNode);
        if(stepReport != null)
            stepReport.setPositiveResponse(result);

        ClientObject expResult = step.getExpectedResult();

        if(!step.isPositive()) 
            throw new ValidationException(new ExpectedNegative());
        if(expResult == null)
            return;

        Collection<ValidationResult> validationResult = compareObjects(expResult, result, step);
        if (!validationResult.isEmpty()) {
            throw new ValidationException(validationResult);
        }
    }

    private void validateFault(Step step, AxisFault ex) throws ValidationException{
        ClientObject result = soapDeserialize(ex);
        if(stepReport != null)
            stepReport.setNegativeResponse(result);
        ClientObject expResult = step.getExpectedResult();
        if(step.isPositive()) 
            throw new ValidationException(new ExpectedPositive());

        if(expResult == null)
            return;

        Collection<ValidationResult> validationResult = compareFaultObjects(expResult, result);
        if (!validationResult.isEmpty()) {
            throw new ValidationException(validationResult);
        }
    }

    private Collection<ValidationResult> compareFaultObjects(ClientObject expResult, ClientObject result) {
        Collection<ValidationResult> validationResult = new ArrayList<ValidationResult>();

        for(Map.Entry<String, Object> field: expResult.getFields().entrySet()) {
            Object expVal = field.getValue();
            Object resultVal = result.getField(field.getKey());

            if(resultVal == null)
				validationResult.add(new MissedField(field.getKey()));
            else if(expVal instanceof String && resultVal instanceof String){
                if(!resultVal.toString().startsWith(expVal.toString()))
	                validationResult.add(new ValueDiffers(field.getKey(), resultVal, expVal));
            } else
				validationResult.add(new UnknownValue(field.getKey(), resultVal, expVal));
        }

        return validationResult;
    }


    private Collection<ValidationResult> compareObjects(ClientObject expResult, ClientObject result, Step step) {

        Collection<ValidationResult> validationResult = new ArrayList<ValidationResult>();

        for(Map.Entry<String, Object> field: expResult.getFields().entrySet()) {
            Object expVal = field.getValue();
            Object resultVal = result.getField(field.getKey());

            if(resultVal == null)
				validationResult.add(new MissedField(field.getKey()));
            else if(expVal instanceof String && resultVal instanceof String){
                if(!compareStrings((String)expVal, (String)resultVal, step.getPrecision())){
                    if (step.isInArray()) {
                        validationResult.add(new ValueDiffers(field.getKey(), VALUE_NOT_FOUND, expVal));
                    } else {
                        validationResult.add(new ValueDiffers(field.getKey(), resultVal, expVal));
                    }
                }
            } else if(expVal instanceof ClientObject && resultVal instanceof ClientObject){
                validationResult.addAll(compareObjects((ClientObject)expVal, (ClientObject)resultVal, step));
            } else if(expVal instanceof List && resultVal instanceof List){
                step.setInArray(Boolean.TRUE);
                compareLists(step, validationResult, field, (List)expVal, (List)resultVal);
                step.setInArray(Boolean.FALSE);
            } else
				validationResult.add(new UnknownValue(field.getKey(), resultVal, expVal));
        }
        return validationResult;
//        Collection<ValidationResult> validationResult = new ArrayList<ValidationResult>();
//
//        for(Map.Entry<String, Object> field: expResult.getFields().entrySet()) {
//            Object expVal = field.getValue();
//            Object resultVal = result.getField(field.getKey());
//
//            if(resultVal == null)
//				validationResult.add(new MissedField(field.getKey()));
//            else if(expVal instanceof String && resultVal instanceof String){
//                if(!compareStrings((String)expVal, (String)resultVal, step.getPrecision()))
//	                validationResult.add(new ValueDiffers(field.getKey(), resultVal, expVal));
//            } else if(expVal instanceof ClientObject && resultVal instanceof ClientObject){
//                  ClientObject expCleantObject = (ClientObject)expVal;
//              if(!expCleantObject.equals(resultVal))  validationResult.add(new ValueDiffers(field.getKey(),expVal,resultVal));
//            } else if(expVal instanceof List && resultVal instanceof List){
//
//                List<ClientObject> listExpVal = (List<ClientObject>)expVal;
//                List<ClientObject> listResVal = (List<ClientObject>)resultVal;
//
//                Set<ClientObject> setExpVal = new HashSet<ClientObject>();
//                Set<ClientObject> setResVal = new HashSet<ClientObject>();
//                for (ClientObject o : listExpVal) {
//                    if (o.getFieldsSize() > 0){
//                        setExpVal.add(o);
//                        //System.out.println(o + "hashCode = " + o.hashCode());
//                    }
//                }
//                for (ClientObject o : listResVal) {
//                    if (o.getFieldsSize() > 0){
//                        setResVal.add(o);
//                        //System.out.println(o + "hashCode = " + o.hashCode());
//                    }
//                }
//                for (ClientObject c : setExpVal){
//                    if(!setResVal.contains(c)) {
//                        //System.out.println(c);
//                        validationResult.add(new ValueDiffers(field.getKey(),c,null));
//                    }
//                }
//               // if(!setResVal.containsAll(setExpVal)) validationResult.add(new ValueDiffers(field.getKey(),expVal,resultVal));
//            }else{
//				validationResult.add(new UnknownValue(field.getKey(), resultVal, expVal));
//            }

//             validationResult.add(new UnknownValue(field.getKey(), resultVal, expVal));

//    }
//        return validationResult;
    }

    private void compareLists(Step step, Collection<ValidationResult> validationResult, Map.Entry<String, Object> field, List listExpVal, List listActVal) {
        if (listExpVal.size() <= listActVal.size()) {
            Object res = checkListsEquals(listExpVal, listActVal, step);
            if (res != null){
//                so strange things needed, because we have some times lists just with strings
                ClientObject expTmp;
                ClientObject actTmp;
                if (res instanceof String){
                    expTmp = new ClientObject();
                    actTmp = new ClientObject();
                    expTmp.addField(field.getKey(), (String) res);
                    actTmp.addField(field.getKey(), (String) listActVal.get(0));
                } else {
                    expTmp = (ClientObject) res;
                    actTmp = (ClientObject) listActVal.get(0);
                }
                validationResult.addAll(compareObjects(expTmp, actTmp, step));
            }
        } else {
            validationResult.add(new ArrayDiffers(field.getKey()));
        }
    }

    private Object checkListsEquals(List<ClientObject> expList, List<ClientObject> actList, Step step) {
        for (Iterator expIt = expList.iterator(); expIt.hasNext();) {
            boolean valuesEquals = false;
            Object expVal = expIt.next();
            for (Iterator actIt = actList.iterator(); actIt.hasNext();) {
                Object actVal = actIt.next();
                if (expVal instanceof ClientObject && actVal instanceof ClientObject) {
                    valuesEquals = checkValuesEquals((ClientObject) expVal, (ClientObject) actVal, step);
                } else if (expVal instanceof String && actVal instanceof String) {
                    valuesEquals = checkValuesEquals((String) expVal, (String) actVal, step);
                }
                if (valuesEquals)
                    break;
            }
            if (!valuesEquals)
                return expVal;
        }
        return null;
    }

    private boolean checkValuesEquals(String exp, String act, Step step) {
       return compareStrings(exp, act, step.getPrecision());
    }

    private boolean checkValuesEquals(ClientObject exp, ClientObject act, Step step) {
        for (Map.Entry<String, Object> field : exp.getFields().entrySet()) {

            Object expVal = field.getValue();
            Object actVal = act.getField(field.getKey());

            if (actVal == null) {
                return false;
            } else if (expVal instanceof String && actVal instanceof String) {
                if (!compareStrings((String) expVal, (String) actVal, step.getPrecision()))
                    return false;
            } else if (expVal instanceof ClientObject && actVal instanceof ClientObject) {
                checkValuesEquals((ClientObject) expVal, (ClientObject) actVal, step);
            } else if (expVal instanceof List && actVal instanceof List) {
                checkListsEquals((List<ClientObject>) expVal, (List<ClientObject>) actVal, step);
            }
        }
        return true;
    }
    
    private boolean compareStrings(String expVal, String resultVal, double precision){
        if(isNumber(expVal)){
            if(!isNumber(resultVal))
                return false;

            double exp = Double.parseDouble(expVal);
            double res = Double.parseDouble(resultVal);

            return Math.abs(exp - res) <= Math.abs(precision);
        }
        return expVal.equals(resultVal);
    }

    private boolean isNumber(String str){
        final String Digits = "(\\p{Digit}+)";
        // an exponent is 'e' or 'E' followed by an optionally
        // signed decimal integer.
        final String Exp = "[eE][+-]?" + Digits;
        final String fpRegex =
            ("[\\x00-\\x20]*" + // Optional leading "whitespace"
            "[+-]?(" + // Optional sign character
            "NaN|" + // "NaN" string
            "Infinity|" + // "Infinity" string

            // Digits ._opt Digits_opt ExponentPart_opt FloatTypeSuffix_opt
            "((" + Digits + "(\\.)?(" + Digits + "?)(" + Exp + ")?)|" +
            // . Digits ExponentPart_opt FloatTypeSuffix_opt
            "(\\.(" + Digits + ")(" + Exp + ")?)|" +
            "[fFdD]?))" +
            "[\\x00-\\x20]*");// Optional trailing "whitespace"

        return Pattern.matches(fpRegex, str);
    }

	private ClientObject soapDeserialize(Node node) {
		ClientObject parent = new ClientObject();
		NodeList children = node.getChildNodes();
		for (int i = 0; i < children.getLength(); ++i) {
			Node child = children.item(i);
			if (child.hasChildNodes() && child.getFirstChild().getNodeType() == Node.TEXT_NODE) {
				parent.addField(child.getLocalName(), child.getFirstChild().getNodeValue());
			} else {
				ClientObject childObject = soapDeserialize(child);
				parent.addField(child.getLocalName(), childObject);
			}
		}
		return parent;
	}

	private ClientObject soapDeserialize(AxisFault ex) {
        ClientObject ret = new ClientObject();
        ret.addField("faultcode", ex.getFaultCode().getLocalPart());
        ret.addField("faultstring", ex.getFaultString());
        return ret;
    }

    private void soapSerialize(ClientObject obj, SOAPElement parent) throws SOAPException{
        for(Map.Entry<String, Object> field: obj.getFields().entrySet()){
            Object val = field.getValue();
            if(val instanceof String || val instanceof ClientObject){
                soapSerializeSimple(field.getKey(), val, parent);
            } else if(val instanceof List){
                for(Object el: (List) val){
                    if(el instanceof String || el instanceof ClientObject){
                        soapSerializeSimple(field.getKey(), el, parent);
                    }
                }
            }
        }
    }

    private void soapSerializeSimple(String fieldName, Object obj, SOAPElement parent) throws SOAPException{
        SOAPElement child = parent.addChildElement(fieldName, configuration.getNamespacePrefix());
        if(obj instanceof String){
            child.addTextNode((String)obj);
        } else if(obj instanceof ClientObject){
            soapSerialize((ClientObject) obj, child);
        }
    }

}

//NB:
        //-------------settign the header
        /*PrefixedQName svcName = new PrefixedQName("http://schemas.xmlsoap.org/ws/2002/07/secext", "Security", "wsse");
        SOAPHeaderElement sh = new SOAPHeaderElement(svcName);
        SOAPElement sub = sh.addChildElement("UsernameToken");
        SOAPElement element = sub.addChildElement("Username");
        element.addTextNode("userName");
        element = sub.addChildElement("Password");
        element.addTextNode("password");
        request.addHeader(sh);*/
        //------------


